Document version: $Id: body.htm,v 1.2 2004/05/03 15:07:15 pvspain Exp $

Introduction

DUnitWizard is a Delphi plug-in that enables you to rapidly construct DUnit test suites. It generates the code for new DUnit projects, modules and class declarations. Tightly integrated into the Delphi IDE, it uses the environment of the active project for naming and location suggestions, and parses the topmost module to generate new test modules and class declarations.

This wizard is a supplement to the DUnit testing framework. To compile and run the test projects created by the wizard, you must first have installed the DUnit framework source, and it must be accessible through:

Code generation is highly customisable, so the output looks the way you want it:

The goal is for the wizard to do all the grunt work, freeing you to concentrate on testing logic.

DUnitWizard comprises three packages:

The remainder of this document deals with the design-time package, EPCDUnitWizardXX.bpl

Terminology

Throughout this documentation you will find references to test classes and tested classes. Test classes are the DUnit classes that contain your testing logic. Tested classes are your original code; they are the subject of the DUnit tests. Another commonly used term for tested class is "class under test".

Testing strategies

DUnit tests are contained in DUnit test projects. Use the project wizard to create a DUnit dpr file. The test classes may be contained in their own modules (units), or embedded in the same module as the associated tested class.

Use the test module wizard to create a new module and test classes based on an existing module you wish to test. Use the test class wizard to add a new test class to an existing test module. Separate test modules mean we are limited to testing globally-scoped classes (declared in the interface section of modules), and only the published and public methods of those classes, or also protected methods using the well-known Delphi class-cracking hack[2].

Embedding a test class in the same module as the tested class gives us access to everything. We can test module-scoped classes (declared in the implementation section) and private and protected methods of the tested class. Some would argue that you should only test the public interface of a class (ie published and public methods), but how you use the tool is your business. Use the test class wizard to create embedded test classes.

Using DUnitWizard

After installing the wizard, the next time you start Delphi you will find a new menu item , DUnit, on the IDE's main menu.

There are also two new DUnit entries on the New page of the New Items applet (IDEMenu|File|New... in D5, IDEMenu|File|New|Other... in D6+). DUnitProject and DUnit TestModule correspond to the first and second DUnit menu items respectively.


Creating a DUnit project

The first item on the DUnit menu, New Project... (Alt+U+P), opens a dialogue to create a new DUnit project. This is the .dpr file that binds your test modules to the DUnit framework code.

The project name and path will be prefilled with suggestions based on the active project in the IDE - specifically, wizard parameters PROJECTNAME and PROJECTPATH respectively. You can override these suggestions, or change the format of the parameters to your liking via the Options dialogue.
Stretch the dialogue sideways to accomodate long paths like this example. The new width will be remembered. The default value of PROJECTNAME is to take the current IDE project and append "Tests".

Example:
The current project in the IDE is "customdraw" so the suggested project name becomes "customdrawTests".

The corresponding project filename is shown in a read-only edit field.The default value of PROJECTPATH is to take the path of the current project file and create a subdirectory called dunit. Any non-existent directories in the path will be automatically created.

Example:
The current project path is "C:\ProgramFiles\Borland\Delphi6\Demos\CustomDraw\",
so the suggested project path becomes "C:\ProgramFiles\Borland\Delphi6\Demos\CustomDraw\dunit\"

There is also a checkbox to add the new test project to the current IDE project group. The setting of this checkbox is remembered, and can also be set via the Options dialogue. Note that a project group must have been previously created. The default project group in Delphi's "bin" directory is merely a placeholder. There is also a quirk/bug in the Delphi IDE which requires that you close the project group after initially creating it, to be able to add new projects. Saving the project group is not sufficient.
I strongly suggest that you use project groups for DUnit testing. They allow convenient grouping of your project and associated DUnit test project and easy switching between the two via the Project Manager (MainMenu|View|Project Manager). I would also suggest that you use Delphi's "desktops" feature and dock the project manager so that it is always visible next to the editor pane. Then it is simply a double-click to switch between your project and DUnit test project, and its immediately obvious which project is active.

Clicking the dialogue's "Create" button will create a new DUnit test project and open the .dpr file in an IDE editor window. For our example, that code is reproduced below. Note that you can switch between the GUI and console modes of DUnit testing by commenting or uncommenting the third line of the file, ie the {$APPTYPE CONSOLE} directive.

// Uncomment the following directive to create a console application
// or leave commented to create a GUI application... 
// {$APPTYPE CONSOLE}
program customdrawTests;
uses
  TestFramework {$IFDEF LINUX},
  QForms,
  QGUITestRunner {$ELSE},
  Forms,
  GUITestRunner {$ENDIF},
  TextTestRunner;
{$R *.RES}
begin
  Application.Initialize;
{$IFDEF LINUX}
  QGUITestRunner.RunRegisteredTests;
{$ELSE}
  if System.IsConsole then
    TextTestRunner.RunRegisteredTests
  else
    GUITestRunner.RunRegisteredTests;
{$ENDIF}
end.

The format of the generated code can be modifed by editing the project text template file compiled into the wizard as a text resource.

Creating a DUnit test module

Now we have a test project, we can start creating test modules. The second item on the DUnit menu, New TestModule... (Alt+U+M), opens a dialogue to configure a test module, based on the topmost unit in the IDE. We will be creating a test module based on zero or more of the globally-scoped classes declared in the topmost (currently selected) unit. The new module will be added to the currently active IDE project, so check that your DUnit test project is active (the bold entry in the IDE Project Manager) before opening this dialogue.


Stretch the dialogue to your liking in either direction and/or move the splitter between the treeviews. The new settings will be remembered.

The Unit name field is prefilled with a suggestion based on the UNITNAME parameter, which defaults to the name of the current IDE unit with "Tests" appended. Of course, you can override the suggestion or change the UNITNAME parameter via the Options dialogue.

Example:
The currently selected IDE unit is "CustomDrawTreeView", so the suggested test module name becomes "CustomDrawTreeViewTests"

The Unit path field is prefilled with a suggestion based on the UNITPATH parameter, which defaults to the path of the current IDE unit with a subdirectory "dunit" appended. As above, you can override the suggestion or reformat the UNITPATH parameter. Any non-existent directories in the path will be automatically created.

Example:
The path of the currently selected IDE unit is: "C:\Program Files\Borland\Delphi6\Demos\CustomDraw\",
so the suggested test module path becomes "C:\Program Files\Borland\Delphi6\Demos\CustomDraw\dunit\"

Next, we have a couple of checkboxes concerning uses clauses. The first one adds the current IDE unit, CustomDrawTreeView in our example to the (interface) uses clause in the new test module. The second checkbox adds the current IDE unit to the "uses" clause in the current project file (.dpr). The setting of these checkboxes will be remembered and can also be pre-configured from the Options dialogue.

Following the checkboxes are two treeviews:
You will notice that every item in the left treeview has an associated checkbox. Clicking those checkboxes immediately adds/removes that item and any children to/from the right treeview, and consequently the generated test classes. There are a few points to note about the right treeview:

Clicking the "Create" button will generate the test module and open it as a new IDE editor window. The generated code for the example module is reproduced here. Note that SetUp and TearDown methods are declared but commented out initially, and that each declared class is registered with the DUnit test framework in the unit's initialization section. Place the text cursor (caret) within the class declaration and use Delphi's code completion feature (Ctrl+Shift+C) to generate the class method stubs in the implementation section of the unit.

unit CustomDrawTreeViewTests;
interface
uses
  CustomDrawTreeView,
  TestFrameWork;
type
  TCustomDrawFormTests = class(TTestCase)
  private
  protected
  //  procedure SetUp; override;
  //  procedure TearDown; override;
  published
    // Test methods
    procedure TestFormCreate;
    procedure TestTVCustomDraw;
    procedure TestTVCustomDrawItem;
    procedure TestTVGetImageIndex;
    procedure TestTVGetSelectedIndex;
    procedure TestExit1Click;
    procedure TestSelection1Click;
    procedure TestColor1Click;
    procedure TestSelectionBackground1Click;
    procedure TestSolid1Click;
    procedure TestNone1Click;
    procedure TestOnCustomDraw1Click;
    procedure TestOnCustomDrawItem1Click;
    procedure TestTVExpanded;
    procedure TestButtonColor1Click;
    procedure TestButtonSize1Click;
    procedure TestDrawing1Click;
    procedure TestColor2Click;
    procedure TestCustomDraw1Click;
    procedure TestFont2Click;
  end;
implementation
initialization
  TestFramework.RegisterTest('CustomDrawTreeViewTests Suite',
    TCustomDrawFormTests.Suite);
end.

The format of the generated code can be modified by editing the relevant text template files compiled into the wizard as a text resource.

Creating a DUnit test class

You can create a test class declaration for any class, by placing the text cursor anywhere within the class declaration, and selecting New Test Class... (Alt+U+C) from the DUnit menu. A dialogue similar to the test module dialogue appears:


The left treeview displays the selected class declaration. Note that you also have the option of selecting private methods.

The right treeview is a preview of the test class declaration, and changes dynamically in response to your manipulation of the left treeview. As before, you can preconfigure the selected class visibilities via the Options dialogue - these selections are distinct from the test module choices.

Exit the dialogue by pressing the "Copy to Clipboard" button. Then paste the new test class declaration wherever you desire, by moving the text cursor to the insertion point and pressing Ctrl+V or selecting IDEMenu|Edit|Paste.

The generated code for the example is shown here:

  type
    TResourceItemTests = class(TTestCase)
    private
protected // procedure SetUp; override; // procedure TearDown; override; published // Test methods procedure TestGetName; procedure TestGetResourceList; procedure TestCreateItem; procedure TestIsList; procedure TestOffset; procedure TestSize; procedure TestRawData; procedure TestResTypeStr; procedure TestSaveToFile; procedure TestSaveToStream; end;

Customisation

You can customise the operation of the DUnitWizard via the Options dialogue and/or by editing the code templates used for code generation, described in the next section.

Let's start with the Options dialogue. Choose Options... (Alt+U+O) from the DUnit menu. The following tabbed dialogue appears:


The three boxes represent each of the three wizards:

All these settings can all be overriden from the respective wizard dialogues.

Clicking on the "Parameters" tab shows the second page, where you can edit the format of the parameters which substitute for their respective namesakes in the wizard-generated projects, modules and classes.

To operate this dialogue first select a parameter from the leftmost listbox. the desciption of the selection appears in the panel below the listbox. Then edit the content of the selected parameter in the edit control topmost on the righthand side. This may contain any combination of text and macros selected from the listbox below the edit control.

To insert a macro, place the cursor at the insertion point in the edit control and double-click a macro, or select a macro then press "Insert". The description panel to the right of the listbox updates as the macro selection changes. The last five macros take an argument, which can be any combination of macros and fixed text. Macros can be nested within macros to any level you desire.

You can further customise DUnitWizard by editing the code templates used for code generation.

Code templates

The project, test module and test class wizards all use code templates. These are parameterised code snippets that are stored as text resources in the design-time package. The project source template is stored in the text resource DUNIT_PROJECTSOURCE_TEXT, derived from ProjectSource.txt. The test module source is stored in the text resource DUNIT_TESTMODULE_TEXT, derived from TestModule.txt. The test module source contains references to fixed parameters which in turn are substituted by other template files. All these templates can be modified by editing the respective text file. You must also modify the corresponding file length value (eg ProjectTextLength for ProjectSource.txt) in DUnitCommon.pas, then recompile and reinstall the design-time package.

Parameters in the code templates are immediately preceded by a hash character '#', eg #UNITNAME. They are all case-insensitive, but are upper-case by convention to highlight their presence. There are both fixed and user-defined parameters.

Fixed parameters

USESTESTEDUNIT The uses clause entry for the currently selected IDE unit in the new test module. The line containing this parameter is replaced by the text resource DUNIT_USESTESTEDUNIT_TEXT from the design-time package, following substitution for any parameters contained in the resource. The resource content can be modifed by editing UsesTestedUnit.txt and recompiling and installing the design-time package.
TESTCLASSDECLBLOCK The test class declaration(s) for the class(es) selected for testing (the tested classes) from the currently selected IDE unit. The line containing this parameter is replaced by one instance of text resource DUNIT_TESTCLASSDECL_TEXT from the design-time package for each test class, following substitution for any parameters contained in the resource. The resource content can be modifed by editing TestClassDecl.txt and recompiling and installing the design-time package.
TESTMETHODBLOCK Locates the position of the test class method declaration block within the DUNIT_TESTMODULE_TEXT resource. The line containing this parameter is replaced by one instance of text resource DUNIT_TESTMETHODDECL_TEXT from the design-time package for each test method, following substitution for any parameters contained in the resource. The resource content can be modifed by editing TestMethodDecl.txt and recompiling and installing the design-time package.
TESTSUITEREGBLOCK The test class registrations with DUnit in the new test module. The line containing this parameter is replaced by one instance of text resource DUNIT_TESTSUITEREG_TEXT from the design-time package for each test class, following substitution for any parameters contained in the resource. The resource content can be modifed by editing TestSuiteReg.txt and recompiling and installing the design-time package.
TESTEDUNITNAME The name of the currently selected IDE unit (no path or extension)
TESTEDUNITPATH The file system absolute path of the directory containing the currently selected IDE unit (includes trailing directory delimiter character)

User-defined parameters

UNITNAME The name of the new DUnit test module (no .pas extension)
UNITPATH The file system absolute path of the directory containing the new test module (includes trailing directory delimiter character)
CLASSNAME The name of a DUnit test class
METHODNAME The name of a DUnit test method
PROJECTNAME The name of the new DUnit test project (no .dpr extension)
PROJECTPATH The file system absolute path of the directory containing the new DUnit test project (includes trailing directory delimiter character)

The user-defined parameters in turn are defined by templates, configured via the Options dialogue. These templates can include macros. They are case-insensitive, but upper-cased by convention, and identified by a prefixed dollar character '$', eg $CURRENTUNIT

Parameter macros

TESTEDCLASSNAME The name of a class from the currently selected IDE unit that is to be tested (no .pas extension).
Note: This macro is only meaningful in the context of the definition of the CLASSNAME parameter.
TESTEDMETHODNAME The name of a method from a tested class.
Note: This macro is only meaningful in the context of the definition of the METHODNAME parameter.
CURRENTUNIT The absolute file specification for the currently selected IDE unit (includes path and filename)
CURRENTPROJECT The absolute file specification for the currently selected IDE project (includes path and filename for the Delphi project file, .dpr)
PROJECTGROUP The absolute file specification for the currently selected IDE project group (includes path and filename for the Delphi project group file, .bpg)
FILEPATH() This macro extracts the path (including trailing directory delimiter) from the filename expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting.
FILENAME() This macro function extracts the filename (including any extension) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting.
FILESTEM() This macro function extracts the filestem (filename excluding extension) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting.
FILEEXT() This macro function extracts the filename extension (including leading period) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting.
ENVIROVAR() This macro returns the current value of the environment variable expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting.

Source code guide

The entry point for any design-time package is a global procedure named Register (case-sensitive). This is contained in XPDUnitWizard.pas. This procedure registers an instance of IOTAWizard, implemented by TXPDUnitWizard. This class creates the project, module, class and menu wizards mentioned earlier, and manages the registration and deregistration of these wizards with the IDE.

I recommend the use of a container wizard like this for OTA plug-ins. Interfaces registered via ToolsAPI.RegisterPackageWizard() must have a reference count of one to be destroyed by the IDE; otherwise, you will very likely get access violations whenever your design-time package is unloaded. Many wizards will implement more than one interface and will consequently have a reference count of two or more at times. Using a container wizard for IDE registration avoids this problem.

The project wizard is declared in XPDUnitProjectWizard.pas. The class TProjectWizard implements the necessary OTA wizard interfaces. The method TProjectWizard.Execute pops up the project dialogue. The dialogue, implemented in XPDUnitProject.pas, loads the wizard parameters and persisted settings in TXPDUnitProjectForm.FormCreate. The wizard parameters are evaluated for the active project in TXPDUnitProjectForm.FormShow. If the 'Create' button is clicked, a new module is created. The class TProjectCreator implements the OTA module interfaces. TProjectCreator.NewProjectSource returns an interface, IOTAFile, implemented by TProjectSource, which provides the source for the dpr file. TProjectSource.Create extracts the template resource from the package, and TProjectSource.GetSource makes the substitutions based on the user's choices from the project dialogue.

The test module wizard is declared in XPDUnitTestModuleWizard.pas. The class TTestModuleWizard implements the necessary OTA wizard interfaces. The method TTestModuleWizard.Execute pops up the test module dialogue. The dialogue, implemented in XPDUnitTestModule.pas, loads the current wizard parameters, persisted settings and creates a module parser and filters in TXPDUnitProjectForm.FormCreate. The module parser is implemented in XPTestedUnitParser.pas, the parser filters in XPParserFilters.pas. There is a parser filter for each of the treeviews in the dialogue. The wizard parameters are evaluated, the unit parsed, and the resultant parse tree filtered for the active module in the IDE in TXPDUnitTestModuleForm.FormShow. If the 'Create' button is clicked, a new module is created. The class TTestModuleCreator implements the OTA module interfaces. The filtered test class parser tree is passed into TTestModuleCreator. TTestModuleCreator.NewImplSource returns an interface, IOTAFile, implemented by TTestModuleSource, which provides the source for the new pas file. TTestModuleSource.Create creates an IXPDUnitTextTemplates interface, implemented in XPTextTemplates.pas, to extract the template resource from the package and make the substitutions based on the user's choices from the test module dialogue.

The test class wizard is declared in XPDUnitTestClassWizard.pas. The class TTestClassWizard implements the necessary OTA wizard interfaces. The method TTestClassWizard.Execute pops up the test module dialogue. The dialogue, implemented in XPDUnitTestClass.pas, loads the current wizard parameters, persisted settings and creates a module parser and filters in TXPDUnitTestClassForm.FormCreate. The same parser as the test module wizard is used again, but different filters to extract only the current class declaration (enclosing the cursor postion in the current IDE unit). If the 'Copy to Clipboard' button is clicked, a new test class declaration is created and copied to the clipboard. TTestClassWizard.Execute creates an IXPDUnitTextTemplates interface, as used by the test module wizard, to extract the template resource from the package and make the substitutions based on the user's choices from the test class dialogue.

The menu wizard is declared in XPDUnitMenuWizard.pas. The class TXPDUnitMenuWizard implements a basic IOTAWizard interface, taking references to the project, test module and test class wizards as arguments to its constructor. TXPDUnitMenuWizard.Create creates the DUnit menu, wires up all the other wizards to their respective menu items and also wires up the Options dialogue and HTML documentation. The destructor TXPDUnitMenuWizard.Destroy reverses these operations.

Frequently asked questions (FAQ)

Please feel free to ask questions via the EPC support web page.

Q: The wizard creates the test modules in a subdirectory below the tested module called 'dunit'. I keep them in the same directory as the tested modules. How can I set this up?

A: Choose IDEMenu|DUnit|Options... Click the Parameters tab and select the UNITPATH parameter. Change the default value $FILEPATH($CURRENTUNIT)dunit\ to $FILEPATH($CURRENTUNIT)

Q: All my source modules and project files are in a directory called 'Source'. All my test modules and test project files are in a directory at the same level called 'Tests'. How can I set this up?

A: Choose IDEMenu|DUnit|Options... Click the Parameters tab and select the PROJECTPATH parameter. Change the default value $FILEPATH($CURRENTPROJECT)dunit\ to $FILEPATH($CURRENTPROJECT)..\Tests\. Select the UNITPATH parameter. Change the default value $FILEPATH($CURRENTUNIT)dunit\ to $FILEPATH($CURRENTPROJECT)..\Tests\

Q: I prefer to use a prefix of TEST_ for all my test method names. How can I set this up?

A: Choose IDEMenu|DUnit|Options... Click the Parameters tab and select the METHODNAME parameter. Change the default value Test$TESTEDMETHODNAME to TEST_$TESTEDMETHODNAME

Q: I always test protected methods as well as published and public. How can I set this up so I don't have to select the protected node each time in the 'New TestModule dialogue'?

A: Choose IDEMenu|DUnit|Options... Click the Behaviour tab and tick the Add PROTECTED method tests checkbox in the New TestModule group box.


Footnotes:

[1] (XX denotes the Delphi version: XX=50: Delphi 5, XX=60: Delphi 6, XX=70: Delphi 7)

[2] Class cracking example:
There is a class TSample in a unit Sample.pas. You can access the protected methods of TSample from another unit, say Test.pas, by declaring an empty subclass of TSample in Test.pas, say TCrackedSample, and casting instances of TSample to TCrackedSample.

In Sample.pas:

type
TSample = class
...
protected procedure AProtectedMethod;
...
end;
In Test.pas:

uses Sample;
...
type TCrackedSample = class(TSample);
...
var ASample: TSample;
...
// call protected method
TCrackedSample(ASample).AProtectedMethod;